/*
* Derived from code included in the Fedora Repository System:
*
* <p>Copyright &copy; 2008 Fedora Commons, Inc.<br />
* <p>Copyright &copy; 2002-2007 The Rector and Visitors of the University of 
* Virginia and Cornell University<br /> 
* All rights reserved.</p>
* 
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* 
* http://www.apache.org/licenses/LICENSE-2.0
* 
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ 

package com.vtls.oai.web;

import com.vtls.oai.domain.MetadataFormat;
import com.vtls.oai.domain.Record;
import com.vtls.oai.domain.SetMembership;
import com.vtls.oai.service.exceptions.BadArgumentException;
import com.vtls.oai.service.exceptions.BadResumptionTokenException;
import com.vtls.oai.service.exceptions.BadVerbException;
import com.vtls.oai.service.exceptions.CannotDisseminateFormatException;
import com.vtls.oai.service.exceptions.NoMetadataFormatsException;
import com.vtls.oai.service.exceptions.ProtocolException;
import com.vtls.oai.service.exceptions.ServerException;
import com.vtls.oai.service.Provider;
import com.vtls.oai.service.Session;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.MultiActionController;

public class ProviderController extends MultiActionController implements MessageSourceAware
{
	private final Log log = LogFactory.getLog(getClass());

	private MessageSource messages = null;
	private Provider provider = null;
   private Map<String, MetadataFormat> metadataFormatMap = new HashMap<String, MetadataFormat>();
   private Map<String, Session> sessions = new HashMap<String, Session>();

   public ProviderController()
   {
      super();
   }

	/* Accessors/Mutators
	 **************************************************************************/

	public void setMessageSource(final MessageSource messages)
	{
		this.messages = messages;
	}

	public MessageSource getMessageSource()
	{
		return this.messages;
	}
   
	public void setProvider(final Provider provider)
	{
		this.provider = provider;
	}

	public Provider getProvider()
	{
		return this.provider;
	}
   
   public List<MetadataFormat> getMetadataFormats() throws NoMetadataFormatsException, ProtocolException
   {
      final List<MetadataFormat> formats = new ArrayList<MetadataFormat>();
      formats.addAll(this.metadataFormatMap.values());
      return formats;
   }

   public void setMetadataFormats(final List<MetadataFormat> formats)
   {
      if(formats == null)
         throw new IllegalArgumentException("'setMetadataFormats()' expects a non-null Collection of MetadataFormat instances.");

      this.metadataFormatMap.clear();
      
      for(MetadataFormat format : formats)
         this.metadataFormatMap.put(format.getMetadataPrefix(), format);
   }

	/* MultiActionController Methods
	 **************************************************************************/

	@Override
   protected void initBinder(final HttpServletRequest request, final ServletRequestDataBinder binder)
   {
      binder.registerCustomEditor(String.class, null, new StringTrimmerEditor(true));
   }

	public ModelAndView defaultHandler(final HttpServletRequest request, final HttpServletResponse response, final Arguments arguments)
	{
		final Map<String, Object> model = new HashMap<String, Object>();

      // Debugging.
      response.setContentType("text/xml; charset=UTF-8");

      // The OAI protocol includes the original HTTP request.
		final String requestURL = request.getRequestURL().toString();
		model.put("RequestURL", requestURL);

      try
      {
         if(this.getProvider() == null)
            throw new ServerException("There was a configuration problem. ProviderController expects a non-null 'provider'.");
            
         // The 'verb' parameter must be set.
         if(arguments.getVerb() == null)
            throw new BadArgumentException("A 'verb' argument is required.");
         
         // Restrict arguments just to the ones defined by the spec.
         final String[] allowedArguments = { "from", "identifier", "metadataPrefix",
            "resumptionToken", "set", "until", "verb" };

         final Set<String> allowedArgumentSet = new HashSet<String>();
         for(String argument : allowedArguments)
            allowedArgumentSet.add(argument);

         Set<String> requestParameters = (Set<String>)request.getParameterMap().keySet();
         for(String parameter : requestParameters)
         {
            if(!allowedArgumentSet.contains(parameter))
               throw new BadArgumentException("Unknown argument '" + parameter + "'.");
         }

         Provider provider = this.getProvider();
         final String verb = arguments.getVerb();
         
         if(verb.equals("GetRecord"))
         {
            // Check required arguments.
            if(arguments.getIdentifier() == null)
               throw new BadArgumentException("An 'identifier' argument is required for this verb.");
            if(arguments.getMetadataPrefix() == null)
               throw new BadArgumentException("A 'metadataPrefix' argument is required for this verb.");

            // Check unsupported arguments.
            if(arguments.getFrom() != null || arguments.getUntil() != null || arguments.getResumptionToken() != null || arguments.getSet() != null)
               throw new BadArgumentException("Only the 'identifier' and 'metadataPrefix' arguments are valid for this verb.");
            
            // Retrieve the requested Record.
            final MetadataFormat format = this.metadataFormatMap.get(arguments.getMetadataPrefix());
            if(format == null)
               throw new CannotDisseminateFormatException();

            final Record record = provider.getRecord(arguments.getIdentifier(), format);
            
            try
            {
               final byte[] metadata = provider.getRecordMetadata(record.getIdentifier(), format);
               if(metadata != null)
                  record.setMetadata(IOUtils.toString(metadata));
            }
            catch(Exception e)
            {
               log.error("There was a problem obtaining metadat for '" + record.getIdentifier() + "'.", e);
            }

            model.put("GetRecord", record);
         }
         else if(verb.equals("ListRecords") || verb.equals("ListIdentifiers"))
         {
            // Check unsupported arguments.
            if(arguments.getIdentifier() != null)
               throw new BadArgumentException("The 'identifier' argument is not valid for this verb.");

            Session session = null;
            
            if(StringUtils.isNotEmpty(arguments.getResumptionToken()))
            {
               if(arguments.getFrom() != null || arguments.getUntil() != null || arguments.getIdentifier() != null || arguments.getSet() != null || arguments.getMetadataPrefix() != null)
                  throw new BadArgumentException("A 'resumptionToken' argument cannot be used in conjunction with any others.");

               session = sessions.get(arguments.getResumptionToken());

               if(session == null)
                  throw new BadResumptionTokenException("The session could not be found.");
               
               sessions.remove(arguments.getResumptionToken());
            }
            else
            {
               // Check required arguments.
               if(StringUtils.isEmpty(arguments.getMetadataPrefix()))
                  throw new BadArgumentException("A 'metadataPrefix' argument is required for this verb.");

               session = new Session();
               session.setMetadataPrefix(arguments.getMetadataPrefix());
               session.setSet(arguments.getSet());

               // Handle dates in the Session.
               final String[] dateFormats = new String[]{"yyyy-MM-dd", "yyyy-MM-dd'T'HH:mm:ss'Z'"};
               if(StringUtils.isNotEmpty(arguments.getFrom()))
               {
                  final Date fromDate = DateUtils.parseDate(arguments.getFrom(), dateFormats);
                  session.setFrom(fromDate);
               }
               if(StringUtils.isNotEmpty(arguments.getUntil()))
               {
                  final Date untilDate = DateUtils.parseDate(arguments.getUntil(), dateFormats);
                  session.setUntil(untilDate);
               }
            }

            List<Record> records = provider.listRecords(session);
            
            // Obtain metadata for this List of Records.
            if(verb.equals("ListRecords"))
            {
               final String metadataPrefix = session.getMetadataPrefix();
            
               final MetadataFormat format = this.metadataFormatMap.get(metadataPrefix);
               if(format == null)
                  throw new CannotDisseminateFormatException();

               for(Record record : records)
               {
                  if(record.getMetadata() != null)
                     continue;

                  try
                  {
                     final byte[] metadata = provider.getRecordMetadata(record.getIdentifier(), format);
                     if(metadata != null)
                        record.setMetadata(IOUtils.toString(metadata));
                  }
                  catch(Exception e)
                  {
                     log.error("There was a problem obtaining metadata for '" + record.getIdentifier() + "'.", e);
                  }
               }
            }

            // If the Session is still active create a new resumption token and save the Session instance.
            // We're specifically not using HttpSession behavior, of course.
            if(!session.isSessionComplete())
            {
               final String resumptionToken = UUID.randomUUID().toString();
               sessions.put(resumptionToken, session);
               model.put("ResumptionToken", resumptionToken);
            }

            model.put("ListRecords", records);
         }
         else if(verb.equals("ListSets"))
         {
            // Check unsupported arguments.
            if(arguments.getFrom() != null || arguments.getUntil() != null || arguments.getSet() != null || arguments.getIdentifier() != null || arguments.getMetadataPrefix() != null)
               throw new BadArgumentException("No arguments are allowed for this verb other than an optional 'resumptionToken'.");

            // TODO: This model dispenses with the ability to use resumption tokens
            // to iterate over sets, which may not be a bad thing.
            model.put("ListSets", provider.listSets());
         }
         else if(verb.equals("Identify"))
         {
            if(requestParameters.size() > 1)
               throw new BadArgumentException("No additional arguments are allowed for this verb.");
         }
         else if(verb.equals("ListMetadataFormats"))
         {
            // Check required arguments.
            if(arguments.getFrom() != null || arguments.getUntil() != null || arguments.getResumptionToken() != null || arguments.getSet() != null || arguments.getMetadataPrefix() != null)
               throw new BadArgumentException("Only the 'identifier' argument (or no argument) is valid for this verb.");

            // Get the list of allowed metadata formats.
            List<MetadataFormat> formats = null;
            if(arguments.getIdentifier() != null)
            {
               formats = new ArrayList<MetadataFormat>();

               // Collect matching prefixes and populate a List with matching
               // MetadataFormat instances.
               final String[] prefixes = provider.getMetadataPrefixes(arguments.getIdentifier());
               for(String prefix : prefixes)
               {
                  final MetadataFormat format = metadataFormatMap.get(prefix);
                  if(format != null)
                     formats.add(format);
               }
            }
            else
               formats = this.getMetadataFormats();
               
            if(formats != null && formats.size() == 0)
               throw new NoMetadataFormatsException();

            model.put("ListMetadataFormats", formats);
         }
         else
         {
            throw new BadArgumentException("The 'verb' argument must be one of (GetRecord|Identify|ListIdentifiers|ListMetadataFormats|ListRecords|ListSets).");
         }
         
   		return new ModelAndView("oai/" + verb, model);
      }
      catch(ProtocolException e)
      {
         model.put("ErrorCode", e.getCode());
         model.put("ErrorMessage", e.getMessage());
   		return new ModelAndView("oai/Error", model);
      }
      catch(ServerException e)
      {
         try
         {
            log.warn("OAI Service Error: " + e.getMessage(), e);
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "OAI Service Error: " + e.getMessage());
         }
         catch(IOException ioe)
         {
            log.warn("Could not send error to client", ioe);
         }
      }
      catch(Throwable th)
      {
         try
         {
            log.warn("Unexpected Error", th);
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unexpected error");
         }
         catch(IOException ioe)
         {
            log.warn("Could not send error to client", ioe);
         }
      }

		return new ModelAndView("oai/Error", model);
	}
}
